/**
 * @hidden
 */

/**
 */
// Pending Approval Object
// Handles approving, rejecting and getting information on pending approvals
//
// Copyright 2015, BitGo, Inc.  All Rights Reserved.
//
import { common } from '@bitgo/sdk-core';
import * as utxolib from '@bitgo/utxo-lib';

import _ from 'lodash';
import { tryPromise } from '../util';

//
// Constructor
//
const PendingApproval = function (bitgo, pendingApproval, wallet) {
  // @ts-expect-error - no implicit this
  this.bitgo = bitgo;
  // @ts-expect-error - no implicit this
  this.pendingApproval = pendingApproval;
  // @ts-expect-error - no implicit this
  this.wallet = wallet;
};

//
// id
// Get the id of this pending approval.
//
PendingApproval.prototype.id = function () {
  return this.pendingApproval.id;
};

//
// ownerType
// Get the owner type (wallet or enterprise)
// Pending approvals can be approved or modified by different scopes (depending on how they were created)
// If a pending approval is owned by a wallet, then it can be approved by administrators of the wallet
// If a pending approval is owned by an enterprise, then it can be approved by administrators of the enterprise
//
PendingApproval.prototype.ownerType = function (params, callback) {
  params = params || {};
  common.validateParams(params, [], [], callback);

  if (this.pendingApproval.walletId) {
    return 'wallet';
  } else if (this.pendingApproval.enterprise) {
    return 'enterprise';
  } else {
    throw new Error('unexpected pending approval owner: neither walletId nor enterprise was present');
  }
};

//
// walletId
// Get the wallet ID that owns / is associated with the pending approval
//
PendingApproval.prototype.walletId = function () {
  return this.pendingApproval.walletId;
};

//
// enterpriseId
// Get the enterprise ID that owns / is associated with the pending approval
//
PendingApproval.prototype.enterpriseId = function () {
  return this.pendingApproval.enterprise;
};

//
// state
// Get the state of the pending approval
//
PendingApproval.prototype.state = function () {
  return this.pendingApproval.state;
};

//
// creator
// Get the id of the user that performed the action resulting in this pending approval
//
PendingApproval.prototype.creator = function () {
  return this.pendingApproval.creator;
};

//
// type
// Get the type of the pending approval (what it approves)
// Example: transactionRequest, tagUpdateRequest, policyRuleRequest
//
PendingApproval.prototype.type = function () {
  if (!this.pendingApproval.info) {
    throw new Error('pending approval info is not available');
  }
  return this.pendingApproval.info.type;
};

//
// type
// Get information about the pending approval
//
PendingApproval.prototype.info = function () {
  return this.pendingApproval.info;
};

//
// approvalsRequired
// get the number of approvals that are required for this pending approval to be approved.
// Defaults to 1 if approvalsRequired doesn't exist on the object
//
PendingApproval.prototype.approvalsRequired = function () {
  return this.pendingApproval.approvalsRequired || 1;
};

//
// url
// Gets the url for this pending approval
//
PendingApproval.prototype.url = function (extra) {
  extra = extra || '';
  return this.bitgo.url('/pendingapprovals/' + this.id() + extra);
};

//
// get
// Refetches this pending approval and returns it
//
PendingApproval.prototype.get = function (params, callback) {
  params = params || {};
  common.validateParams(params, [], [], callback);

  const self = this;
  return Promise.resolve(this.bitgo.get(this.url()).result())
    .then(function (res) {
      self.pendingApproval = res;
      return self;
    })
    .then(callback)
    .catch(callback);
};

//
// Helper function to ensure that self.wallet is set
//
PendingApproval.prototype.populateWallet = function () {
  const self = this;
  if (!self.wallet) {
    return self.bitgo
      .wallets()
      .get({ id: self.info().transactionRequest.sourceWallet })
      .then(function (wallet) {
        if (!wallet) {
          throw new Error('unexpected - unable to get wallet using sourcewallet');
        }
        self.wallet = wallet;
      });
  }

  if (self.wallet.id() !== self.info().transactionRequest.sourceWallet) {
    throw new Error('unexpected source wallet for pending approval');
  }

  return Promise.resolve(); // otherwise returns undefined
};

//
// helper function to recreate and sign a transaction on a wallet
// we should hopefully be able to move this logic server side soon
//
PendingApproval.prototype.recreateAndSignTransaction = function (params, callback) {
  params = _.extend({}, params);
  common.validateParams(params, ['txHex'], [], callback);

  const transaction = utxolib.bitgo.createTransactionFromHex(params.txHex, utxolib.networks.bitcoin);
  if (!transaction.outs) {
    throw new Error('transaction had no outputs or failed to parse successfully');
  }

  const network = utxolib.networks[common.Environments[this.bitgo.getEnv()].network];
  params.recipients = {};

  const self = this;

  return tryPromise(function () {
    if (self.info().transactionRequest.recipients) {
      // recipients object found on the pending approvals - use it
      params.recipients = self.info().transactionRequest.recipients;
      return;
    }
    if (transaction.outs.length <= 2) {
      transaction.outs.forEach(function (out) {
        const outAddress = utxolib.address.fromOutputScript(out.script, network);
        if (self.info().transactionRequest.destinationAddress === outAddress) {
          // If this is the destination, then spend to it
          params.recipients[outAddress] = out.value;
        }
      });
      return;
    }

    // This looks like a sendmany
    // Attempt to figure out the outputs by choosing all outputs that were not going back to the wallet as change addresses
    return self.wallet.addresses({ chain: 1, sort: -1, limit: 500 }).then(function (result) {
      const changeAddresses = _.keyBy(result.addresses, 'address');
      transaction.outs.forEach(function (out) {
        const outAddress = utxolib.address.fromOutputScript(out.script, network);
        if (!changeAddresses[outAddress]) {
          // If this is not a change address, then spend to it
          params.recipients[outAddress] = out.value;
        }
      });
    });
  }).then(function () {
    return self.wallet.createAndSignTransaction(params);
  });
};

//
// constructApprovalTx
// constructs/signs a transaction for this pending approval, returning the txHex (but not sending it)
//
PendingApproval.prototype.constructApprovalTx = function (params, callback) {
  params = params || {};
  common.validateParams(params, [], ['walletPassphrase'], callback);

  if (this.type() === 'transactionRequest' && !(params.walletPassphrase || params.xprv)) {
    throw new Error('wallet passphrase or xprv required to approve a transactionRequest');
  }

  if (params.useOriginalFee) {
    if (!_.isBoolean(params.useOriginalFee)) {
      throw new Error('invalid type for useOriginalFeeRate');
    }
    if (params.fee || params.feeRate || params.feeTxConfirmTarget) {
      throw new Error('cannot specify a fee/feerate/feeTxConfirmTarget as well as useOriginalFee');
    }
  }

  const self = this;
  return tryPromise(function () {
    if (self.type() === 'transactionRequest') {
      const extendParams: any = { txHex: self.info().transactionRequest.transaction };
      if (params.useOriginalFee) {
        extendParams.fee = self.info().transactionRequest.fee;
      }
      return self.populateWallet().then(function () {
        return self.recreateAndSignTransaction(_.extend(params, extendParams));
      });
    }
  });
};

//
// approve
// sets the pending approval to an approved state
//
PendingApproval.prototype.approve = function (params, callback) {
  params = params || {};
  common.validateParams(params, [], ['walletPassphrase', 'otp'], callback);

  let canRecreateTransaction = true;
  if (this.type() === 'transactionRequest') {
    if (!params.walletPassphrase && !params.xprv) {
      canRecreateTransaction = false;
    }

    // check the wallet balance and compare it with the transaction amount and fee
    if (_.isUndefined(params.forceRecreate) && _.isObject(_.get(this, 'wallet.wallet'))) {
      const requestedAmount = this.pendingApproval.info.transactionRequest.requestedAmount || 0;
      const walletBalance = this.wallet.wallet.spendableBalance;
      const delta = Math.abs(requestedAmount - walletBalance);
      if (delta <= 10000) {
        // it's a sweep because we're within 10k satoshis of the wallet balance
        canRecreateTransaction = false;
      }
    }
  }

  const self = this;
  return tryPromise(function () {
    if (self.type() === 'transactionRequest') {
      if (params.tx) {
        // the approval tx was reconstructed and explicitly specified - pass it through
        return {
          tx: params.tx,
        };
      }

      // this user may not have spending privileges or a passphrase may not have been passed in
      if (!canRecreateTransaction) {
        return {
          tx: self.info().transactionRequest.transaction,
        };
      }

      return self.populateWallet().then(function () {
        const recreationParams = _.extend(
          {},
          params,
          { txHex: self.info().transactionRequest.transaction },
          self.info().transactionRequest.buildParams
        );
        // delete the old build params because we want 'recreateAndSign' to recreate the transaction
        delete recreationParams.fee;
        delete recreationParams.unspents;
        delete recreationParams.txInfo;
        delete recreationParams.estimatedSize;
        delete recreationParams.changeAddresses;
        return self.recreateAndSignTransaction(recreationParams);
      });
    }
  })
    .then(function (transaction) {
      const approvalParams: any = { state: 'approved', otp: params.otp };
      if (transaction) {
        approvalParams.tx = transaction.tx;
      }
      return Promise.resolve(self.bitgo.put(self.url()).send(approvalParams).result()).then(callback).catch(callback);
    })
    .catch(function (error) {
      if (
        !canRecreateTransaction &&
        (error.message.indexOf('could not find unspent output for input') !== -1 ||
          error.message.indexOf('transaction conflicts with an existing transaction in the send queue') !== -1)
      ) {
        throw new Error('unspents expired, wallet passphrase or xprv required to recreate transaction');
      }
      if (
        _.isUndefined(params.forceRecreate) &&
        error.message.indexOf('could not find unspent output for input') !== -1
      ) {
        // if the unspents can't be found, we must retry with a newly constructed transaction, so we delete the tx and try again
        // deleting params.tx will force the code to reach the 'recreateAndSignTransaction' function
        delete params.tx;
        params.forceRecreate = true;
        self.approve(params, callback);
      } else {
        throw error;
      }
    });
};

//
// rejected
// sets the pending approval to a rejected state
//
PendingApproval.prototype.reject = function (params, callback) {
  params = params || {};
  common.validateParams(params, [], [], callback);

  return Promise.resolve(this.bitgo.put(this.url()).send({ state: 'rejected' }).result())
    .then(callback)
    .catch(callback);
};

//
// cancel
// rejects the pending approval
//
PendingApproval.prototype.cancel = function (params, callback) {
  return this.reject(params, callback);
};

export = PendingApproval;
